import asyncio
import os
import json
import sys
from typing import Optional
from openai import OpenAI
from dotenv import load_dotenv
from contextlib import AsyncExitStack   # 用于安全管理异步资源(如 MCP 连接)

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

# 加载 .env文件
load_dotenv()

class MCPClient:
    def __init__(self):
        """初始化 MCP 客户端"""
        self.exit_stack = AsyncExitStack()    # 统一管理异步上下文
        self.openai_api_key = os.getenv("OPENAI_API_KEY")
        self.base_url = os.getenv("BASE_URL")
        self.model = os.getenv("MODEL")
        if not self.openai_api_key:
            raise ValueError("xxx 未找到OpenAI API Key,请在.env文件中设置")

        # 增加超时时间到 120 秒
        self.client = OpenAI(api_key=self.openai_api_key,
                             base_url=self.base_url,
                             timeout=120)
        self.session: Optional[ClientSession] = None     # 用于保存 MCP 客户端会话，默认为None，后面通过connect_to_server进行连接

    async def connect_to_server(self, server_script_path:str):
        """连接到 MCP 服务器并列出可用工具"""
        is_python = server_script_path.endswith(".py")
        is_js = server_script_path.endswith(".js")

        if not (is_python or is_js):
            raise ValueError("服务器脚本必须是 .py 或 .js文件")

        command = "python" if is_python else "node"
        server_params = StdioServerParameters(      # 告诉 MCP 客户端如何启动服务器
            command=command,            # 如python
            args=[server_script_path],  # 如["weather_server.py"]
            env=None
        )
        # 启动 MCP 服务器并建立通信
        stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))        # stdio_client(server_params) 启动服务器进程，并建立标准I/O通信管道
        self.stdio, self.write = stdio_transport    # 拿到读写流
        self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) # ClientSession(self.stdio, self.write) 创建MCP客户端会话，与服务端交互

        await self.session.initialize()             # 发送初始化消息到服务器

        # 列出 MCP 服务器上的工具
        response = await self.session.list_tools()    # 向MCP服务器请求所有已注册的工具(用@mcp.tool())
        tools = response.tools
        print("\n已连接到服务器，支持以下工具:", [tool.name for tool in tools])

    async def process_query(self, query: str) -> str:
        """使用大模型处理查询并调用可用的 MCP 工具 (Function calling)"""

        messages = [{"role": "user", "content": query}]

        response = await self.session.list_tools()      # 获取服务器上的工具

        avaliable_tools = [{
            "type":"function",
            "function":{
                "name":tool.name,
                "description":tool.description,
                "input_schema":tool.inputSchema
            }
        }for tool in response.tools]

        response = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            tools=avaliable_tools
        )

        # 处理返回的内容
        content = response.choices[0]
        if content.finish_reason == "tool_calls":
            # 如果是需要使用工具，就解析工具
            tool_call = content.message.tool_calls[0]
            tool_name = tool_call.function.name
            tool_args = json.loads(tool_call.function.arguments)

            # 执行工具
            result = await self.session.call_tool(tool_name, tool_args)
            print(f"\n\n[Calling tool]{tool_name} with args {tool_args}")

            # 将模型返回的调用哪个工具数据和工具执行完成后的数据都存入messages中
            messages.append(content.message.model_dump())
            messages.append({"role": "tool",
                             "content": result.content[0].text,
                             "tool_call_id": tool_call.id})

            # 将上面的结果再返回给大模型用于生产最终的结果
            response = self.client.chat.completions.create(
                model=self.model,
                messages=messages
            )
            return response.choices[0].message.content

        return content.message.content

    async def chat_loop(self):
        """运行交互式聊天循环"""
        print("\n===MCP 客户端已启动！输入 'quit' 退出===")

        while True:
            try:
                query = input("\n你: ").strip()
                if query.lower() == 'quit':
                    break
                response = await self.process_query(query)  # 调用Open API
                print(f"\n === OpenAI:{response}")
            except Exception as e:
                print(f"\n⚠️ 发生错误: {str(e)}")

    async def cleanup(self):
        """清理资源"""
        await self.exit_stack.aclose()


async def main():
    if len(sys.argv) < 2:
        print("Usage:python client.py <path_to_server_script>")
        sys.exit()

    client = MCPClient()
    try:
        await client.connect_to_server(server_script_path=sys.argv[1])
        await client.chat_loop()
    finally:
        await client.cleanup()


if __name__ == "__main__":
    import sys
    asyncio.run(main())